msg_tool\scripts\bgi\image/
img.rs

1//! Buriko General Interpreter/Ethornell Uncompressed Image File
2use crate::ext::io::*;
3use crate::scripts::base::*;
4use crate::types::*;
5use crate::utils::img::*;
6use anyhow::Result;
7
8fn try_parse(buf: &[u8]) -> Result<u8> {
9    let mut reader = MemReaderRef::new(buf);
10    let width = reader.read_u16()?;
11    let height = reader.read_u16()?;
12    let bpp = reader.read_u16()?;
13    let _flag = reader.read_u16()?;
14    let padding = reader.read_u64()?;
15    if padding != 0 {
16        return Err(anyhow::anyhow!("Invalid padding: {}", padding));
17    }
18    if width == 0 || height == 0 {
19        return Err(anyhow::anyhow!("Invalid dimensions: {}x{}", width, height));
20    }
21    if width > 4096 || height > 4096 {
22        return Err(anyhow::anyhow!(
23            "Dimensions too large: {}x{}",
24            width,
25            height
26        ));
27    }
28    if bpp != 8 && bpp != 24 && bpp != 32 {
29        return Err(anyhow::anyhow!("Unsupported BPP: {}", bpp));
30    }
31    Ok(1)
32}
33
34#[derive(Debug)]
35/// Builder for BGI Uncompressed Image scripts.
36pub struct BgiImageBuilder {}
37
38impl BgiImageBuilder {
39    /// Creates a new instance of `BgiImageBuilder`.
40    pub const fn new() -> Self {
41        BgiImageBuilder {}
42    }
43}
44
45impl ScriptBuilder for BgiImageBuilder {
46    fn default_encoding(&self) -> Encoding {
47        Encoding::Cp932
48    }
49
50    fn build_script(
51        &self,
52        data: Vec<u8>,
53        _filename: &str,
54        _encoding: Encoding,
55        _archive_encoding: Encoding,
56        config: &ExtraConfig,
57        _archive: Option<&Box<dyn Script>>,
58    ) -> Result<Box<dyn Script>> {
59        Ok(Box::new(BgiImage::new(data, config)?))
60    }
61
62    fn extensions(&self) -> &'static [&'static str] {
63        &[]
64    }
65
66    fn script_type(&self) -> &'static ScriptType {
67        &ScriptType::BGIImg
68    }
69
70    fn is_image(&self) -> bool {
71        true
72    }
73
74    fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
75        if buf_len >= 0x10 {
76            return try_parse(&buf[0..0x10]).ok();
77        }
78        None
79    }
80
81    fn can_create_image_file(&self) -> bool {
82        true
83    }
84
85    fn create_image_file<'a>(
86        &'a self,
87        data: ImageData,
88        writer: Box<dyn WriteSeek + 'a>,
89        options: &ExtraConfig,
90    ) -> Result<()> {
91        create_image(data, writer, options.bgi_img_scramble.unwrap_or(false))
92    }
93}
94
95#[derive(Debug)]
96/// BGI Uncompressed Image script.
97pub struct BgiImage {
98    data: MemReader,
99    width: u32,
100    height: u32,
101    color_type: ImageColorType,
102    is_scrambled: bool,
103    opt_is_scrambled: Option<bool>,
104}
105
106fn create_image<'a>(
107    mut data: ImageData,
108    mut writer: Box<dyn WriteSeek + 'a>,
109    scrambled: bool,
110) -> Result<()> {
111    writer.write_u16(data.width as u16)?;
112    writer.write_u16(data.height as u16)?;
113    if data.depth != 8 {
114        return Err(anyhow::anyhow!("Unsupported image depth: {}", data.depth));
115    }
116    match data.color_type {
117        ImageColorType::Bgr => {}
118        ImageColorType::Bgra => {}
119        ImageColorType::Grayscale => {}
120        ImageColorType::Rgb => {
121            convert_rgb_to_bgr(&mut data)?;
122        }
123        ImageColorType::Rgba => {
124            convert_rgba_to_bgra(&mut data)?;
125        }
126    }
127    let bpp = data.color_type.bpp(8);
128    writer.write_u16(bpp)?;
129    let flag = if scrambled { 1 } else { 0 };
130    writer.write_u16(flag)?;
131    writer.write_u64(0)?; // Padding
132    let stride = data.width as usize * ((data.color_type.bpp(8) as usize + 7) / 8);
133    let buf_size = stride * data.height as usize;
134    if scrambled {
135        let bpp = data.color_type.bpp(1) as usize;
136        for i in 0..bpp {
137            let mut dst = i;
138            let mut incr = 0u8;
139            let mut h = data.height;
140            while h > 0 {
141                for _ in 0..data.width {
142                    writer.write_u8(data.data[dst].wrapping_sub(incr))?;
143                    incr = data.data[dst];
144                    dst += bpp;
145                }
146                h -= 1;
147                if h == 0 {
148                    break;
149                }
150                dst += stride;
151                let mut pos = dst;
152                for _ in 0..data.width {
153                    pos -= bpp;
154                    writer.write_u8(data.data[pos].wrapping_sub(incr))?;
155                    incr = data.data[pos];
156                }
157                h -= 1;
158            }
159        }
160    } else {
161        // PNG sometimes return more padding data than expected
162        // We will write only the required size
163        writer.write_all(&data.data[..buf_size])?;
164    }
165    Ok(())
166}
167
168impl BgiImage {
169    /// Creates a new instance of `BgiImage` from a buffer.
170    ///
171    /// * `buf` - The buffer containing the script data.
172    /// * `config` - Extra configuration options.
173    pub fn new(buf: Vec<u8>, config: &ExtraConfig) -> Result<Self> {
174        let mut reader = MemReader::new(buf);
175        let width = reader.read_u16()? as u32;
176        let height = reader.read_u16()? as u32;
177        let bpp = reader.read_u16()?;
178        let color_type = match bpp {
179            8 => ImageColorType::Grayscale,
180            24 => ImageColorType::Bgr,
181            32 => ImageColorType::Bgra,
182            _ => return Err(anyhow::anyhow!("Unsupported BPP: {}", bpp)),
183        };
184        let flag = reader.read_u16()?;
185        let padding = reader.read_u64()?;
186        if padding != 0 {
187            return Err(anyhow::anyhow!("Invalid padding: {}", padding));
188        }
189        let is_scrambled = flag != 0;
190
191        Ok(BgiImage {
192            data: reader,
193            width,
194            height,
195            color_type,
196            is_scrambled,
197            opt_is_scrambled: config.bgi_img_scramble,
198        })
199    }
200}
201
202impl Script for BgiImage {
203    fn default_output_script_type(&self) -> OutputScriptType {
204        OutputScriptType::Json
205    }
206
207    fn default_format_type(&self) -> FormatOptions {
208        FormatOptions::None
209    }
210
211    fn is_image(&self) -> bool {
212        true
213    }
214
215    fn export_image(&self) -> Result<ImageData> {
216        let stride = self.width as usize * ((self.color_type.bpp(8) as usize + 7) / 8);
217        let buf_size = stride * self.height as usize;
218        let mut data = Vec::with_capacity(buf_size);
219        data.resize(buf_size, 0);
220        if self.is_scrambled {
221            let mut reader = self.data.to_ref();
222            reader.pos = 0x10;
223            let bpp = self.color_type.bpp(1) as usize;
224            for i in 0..bpp {
225                let mut dst = i;
226                let mut incr = 0u8;
227                let mut h = self.height;
228                while h > 0 {
229                    for _ in 0..self.width {
230                        incr = incr.wrapping_add(reader.read_u8()?);
231                        data[dst] = incr;
232                        dst += bpp;
233                    }
234                    h -= 1;
235                    if h == 0 {
236                        break;
237                    }
238                    dst += stride;
239                    let mut pos = dst;
240                    for _ in 0..self.width {
241                        pos -= bpp;
242                        incr = incr.wrapping_add(reader.read_u8()?);
243                        data[pos] = incr;
244                    }
245                    h -= 1;
246                }
247            }
248        } else {
249            self.data.cpeek_exact_at(0x10, &mut data)?;
250        }
251        Ok(ImageData {
252            width: self.width,
253            height: self.height,
254            color_type: self.color_type,
255            depth: 8,
256            data,
257        })
258    }
259
260    fn import_image<'a>(&'a self, data: ImageData, file: Box<dyn WriteSeek + 'a>) -> Result<()> {
261        create_image(
262            data,
263            file,
264            self.opt_is_scrambled.unwrap_or(self.is_scrambled),
265        )
266    }
267}